로딩 중이에요... 🐣
[코담]
웹개발·실전 프로젝트·AI까지, 파이썬·장고의 모든것을 담아낸 강의와 개발 노트
32 1 React + Django(DRF+Djoser) JWT 인증 시스템 구현 | ✅ 편저: 코담 운영자
- Djoser를 활용한 인증 시스템 구축 & JWT 베스트 프랙티스📌 본 문서는 React(Vite)와 Django Rest Framework(Djoser)를 활용해 로그인/로그아웃 기능을 포함한
JWT 인증 시스템을 구축하는 전체 과정을 설명합니다.
📁 프로젝트 구조
Chapter32_djoser/
├── backend/ ← Django 백엔드 (Djoser 포함)
└── frontend/ ← React 프론트엔드 (Vite 기반)
1️⃣ 백엔드 설정 (Django + DRF + Djoser)
1-1. Djoser 및 SimpleJWT 설치
pip install djoser djangorestframework djangorestframework-simplejwt
1-2. settings.py
설정
INSTALLED_APPS = [
...
'rest_framework',
'rest_framework_simplejwt',
'djoser',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
SIMPLE_JWT = {
"AUTH_HEADER_TYPES": ("Bearer",), # "Bearer" 여야 React 쪽과 호환됨
}
DJOSER = {
"LOGIN_FIELD": "username", # 또는 "email"
}
✅ 1-3. urls.py
설정
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('auth/', include('djoser.urls')),
path('auth/', include('djoser.urls.jwt')),
]
🔖 Djoser 의 공식문서에는 re_path 를 사용하고 있으나 re_path
대신 path
사용
re_path
는 정규표현식을 위한 것이므로 현재처럼 고정된 경로(auth/
)에는 path()
를 사용하는 것이 더 간결하고 명확합니다.
✔️ 위 urls.py
설정 Djoser가 제공하는 주요 JWT 엔드포인트 확인
이 설정으로 다음과 같은 엔드포인트들이 활성화됩니다:
Method | URL | 설명 |
---|---|---|
POST | /auth/jwt/create/ |
로그인 (access, refresh 토큰 발급) |
POST | /auth/jwt/refresh/ |
access 토큰 갱신 |
POST | /auth/jwt/verify/ |
토큰 유효성 확인 |
GET | /auth/users/me/ |
현재 로그인한 사용자 정보 조회 |
1-4. Django User 생성
python manage.py createsuperuser
1.5 api.http 테스트 실행
api.http 파일
### JWT 로그인
POST http://localhost:8000/auth/jwt/create/ HTTP/1.1
Content-Type: application/json
{
"username": "admin",
"password": "1111"
}
### 토큰 변수 설정
@refreshToken = 발급받은 갱신토큰값
@accessToken = 발급받은 접근토큰값
####################### JWT
GET http://localhost:8000/auth/users/me/ HTTP/1.1
Content-Type: application/json
Authorization: Bearer {{accessToken}}
### verify 유효성 체크
POST http://localhost:8000/auth/jwt/verify/ HTTP/1.1
Content-Type: application/json
Authorization: Bearer {{accessToken}}
{
"token": "{{refreshToken}}"
}
### JWT 토큰 갱신
POST http://localhost:8000/auth/jwt/refresh/ HTTP/1.1
Content-Type: application/json
{
"refresh": "{{refreshToken}}"
}
2️⃣ 프론트엔드 설정 (React + Vite)
1-1. Vite 기반 React 프로젝트 생성
npm create vite@latest frontend -- --template react
cd frontend
npm install
1-2. React Router 설치
npm install react-router-dom
1-3. 인증 Context 구현 (AuthContext.jsx
)
// src/context/AuthContext.tsx
import React, { createContext, useState, useEffect } from "react";
// Context 타입 정의
interface AuthContextType {
user: any;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
}
interface AuthProviderProps {
children: React.ReactNode;
}
// Context 생성
export const AuthContext = createContext<AuthContextType | null>(null);
// Provider 컴포넌트
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState(null);
// 로그인 함수
const login = async (username: string, password: string) => {
try {
const res = await fetch("http://localhost:8000/auth/jwt/create/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
if (!res.ok) throw new Error("Login failed");
const data = await res.json();
localStorage.setItem("token", data.access);
// 유저 정보 가져오기
const userRes = await fetch("http://localhost:8000/auth/users/me/", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${data.access}`,
},
});
if (!userRes.ok) throw new Error("User fetch failed");
const userData = await userRes.json();
setUser(userData);
return true;
} catch (err) {
console.error("로그인 실패:", err);
return false;
}
};
// 로그아웃 함수
const logout = () => {
localStorage.removeItem("token");
setUser(null);
};
// 초기화 함수 (새로고침 시 토큰이 있으면 유저 상태 복원)
const initialize = async () => {
const token = localStorage.getItem("token");
if (!token) return;
try {
const res = await fetch("http://localhost:8000/auth/users/me/", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) throw new Error();
const userData = await res.json();
setUser(userData);
} catch {
localStorage.removeItem("token");
}
};
useEffect(() => {
initialize();
}, []);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};
1.4 라우터 설정 (App.jsx
)
// src/App.tsx
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import { Login } from "./components/Login";
import { Dashboard } from "./components/Dashboard";
function App() {
return (
<AuthProvider>
<BrowserRouter>
<nav style={{ display: "flex", gap: "1rem", marginBottom: "1rem" }}>
<Link to="/login">Login</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
export default App;
1-5 로그인 페이지 (Login.jsx
)
// src/components/Login.tsx
import React, { useState, useContext } from "react";
import { useNavigate } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
export const Login: React.FC = () => {
const authContext = useContext(AuthContext);
if (!authContext) {
throw new Error(
"AuthContext가 정의되지 않았습니다. AuthProvider 내에서 Login을 사용하고 있는지 확인하세요."
);
}
const { login } = authContext;
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const success = await login(username, password);
if (success) navigate("/dashboard");
};
return (
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "0.5rem", width: "250px" }}>
<h2>Login</h2>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
};
1-6 대시보드 페이지 (Dashboard.jsx
)
// src/components/Dashboard.tsx
import React, { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
export const Dashboard: React.FC = () => {
const authContext = useContext(AuthContext);
if (!authContext || !authContext.user) {
return <Navigate to="/login" replace />;
}
const { user, logout } = authContext;
return (
<div style={{ padding: "1rem" }}>
<h2>Welcome, {user.username}</h2>
<button onClick={logout}>Logout</button>
</div>
);
};
✅ 최종 요약
-
JWT 로그인 및 토큰 저장(localStorage)
-
로그인 상태 유지(useEffect + 초기 토큰 확인)
-
보호된 페이지 접근(Dashboard에서 로그인 여부 확인)
-
React Router로 페이지 이동 처리